package onion;

import java.awt.ComponentOrientation;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.swing.text.html.StyleSheet;
import onion.*;
/**
 *
 * @author xDEMONx
 * Клиентское приложение
 */
public class Client extends javax.swing.JFrame {
    //IP-адрес сервера каталогов; Имя конфигурационного файла, формата:
    //     -IP-адрес сервера каталогов
    //     -Его порт
    //     -Желаемый порт для клиента
    //     -Желаемая длина пути
    static String IPSereverDB = "127.0.0.1", ConfFile = "client.conf";    
    //Порт сервера каталогов; Порт клиента
    static int PortServerDB = 1315, myPort = 7161;
    //Желаемая длина пути
    static int lenPath = 3;
    static boolean Started = true;
    //Список имеющихся узлов
    NodeAddress[] list;
    //Используемый сейчас путь
    int[] path;
    //Список симметричных ключей
    SecretKey[] keys;
    ServerSocket clientSoket;
    Socket toNode;
    DataOutputStream toNodeOut;
    //Поток для получения сообщений
    class ListenerThread extends Thread{      
        public void run(){     
            while(true){
                byte[] buf = new byte[OnionConst.max_size];
                try{
                    Socket s = clientSoket.accept();
                    DataInputStream in = new DataInputStream(s.getInputStream());
                    int count_byte = in.read(buf);
                    String str = new String(buf, 0, count_byte, OnionConst.CharSet);
                    tbOutput.append("\n("+s.getInetAddress().getHostAddress()+":"+
                            s.getPort()+"): "+
                            str+"\n");
                }
                catch(Exception e){ 
                    System.out.println("Error: " + e);
                }
                tbOutput.setCaretPosition(tbOutput.getDocument().getLength());             
            }
        }
    }
    
    //Выбор пути передачи данных
    int[] genPath(int len){
        int[] res = new int[len];
        boolean[] used = new boolean[list.length];
        for(int i = 0; i < used.length; i++){
            used[i] = false;
        }
        SecureRandom rnd = new SecureRandom();
        byte[] b = new byte[1];
        for(int i = 0; i < len; i++){            
            rnd.nextBytes(b);
            int k = (b[0] & 0xff)%(list.length - i);            
            int j = -1;
            for(int l = 0; l<list.length && j<k; l++){
                if(!used[l]){
                    j++;
                    if(j==k){
                        used[l] = true;                       
                        res[i] = l;
                    }
                }
            }            
        }
        return res;
    }
    //Генерация симметричных ключей для цепочки
    void genKeys(){
        keys = new SecretKey[path.length];
        try{
            KeyGenerator kg = KeyGenerator.getInstance(OnionConst.SymmeetricKeyEncrypt);
            kg.init(OnionConst.SKE_keysize);
            for(int i = 0; i < keys.length; i++){
                keys[i] = kg.generateKey();
            }
        }catch(Exception e){};
    }
    //Создание и отправка луковицы для пересылки симметричных ключей
    void sendOnion(){
        genKeys();  //Создаём симметричные ключи
        byte[] res = new byte[0];  //Здесь будет луковица
        String lastIp = "";
        int lastPort = 0;    
        try{
            for(int i = path.length - 1; i>=0; i--){
                int cur = path[i];
                byte[] layer;
                if(i<path.length - 1){
                    //Если не последний слой
                    layer = new byte[6+OnionConst.SKE_keysize/8];
                    byte[] tmpIP = Inet4Address.getByName(lastIp).getAddress();
                    for(int j = 0; j < 4; j++){
                        layer[j] = tmpIP[j];
                    }
                    layer[4] = (byte)(lastPort & 0xff);
                    layer[5] = (byte) ((lastPort>>8) & 0xff);
                    byte[] tmpKey = keys[i].getEncoded();
                    for(int j = 0 ; j < tmpKey.length; j++){
                        layer[6+j] = tmpKey[j];
                    }
                }
                else{
                    //Самый последный слой (сердцевинка)
                    layer = keys[i].getEncoded();
                }
                byte[] tmp = new byte[layer.length + res.length];
                for(int j = 0; j < tmp.length; j++){
                    if(j<layer.length){
                        tmp[j] = layer[j];
                    }
                    else{
                        tmp[j] = res[j-layer.length];
                    }
                }
                Cipher RSA_E = Cipher.getInstance(OnionConst.PublicKeyEncrypt);                
                RSA_E.init(Cipher.ENCRYPT_MODE, list[cur].PK);
                int rounds = (tmp.length+OnionConst.max_size_block_encrypt-1)
                        /OnionConst.max_size_block_encrypt;
                res = new byte[rounds*OnionConst.block_size_decrypt];
                int pos = 0;
                int cur_for_res=0;
                for(int j = 0; j < rounds; j++){
                    byte[] tmpCT = RSA_E.doFinal(Arrays.copyOfRange(tmp, pos, 
                            Math.min(pos+OnionConst.max_size_block_encrypt,tmp.length)));
                    pos+=OnionConst.max_size_block_encrypt;
                    for(int l = 0; l < tmpCT.length; l++){
                        res[cur_for_res++] = tmpCT[l];
                    }
                }                        
                lastIp = list[cur].IP;
                lastPort = list[cur].port;                    
            }
            if(path.length>0){
                toNode = new Socket(lastIp, lastPort);
                toNodeOut = new DataOutputStream(toNode.getOutputStream());
                toNodeOut.write(res);
                toNodeOut.flush();
            }
        }catch(Exception e){tbOutput.setText("Error : "+e);};
    }
    /**
     * Creates new form Client
     */
    public Client() {
        initComponents();
        //Инициализация     
        myInit();
        Started = false;
    }    
    //Функция инициализации
    void myInit(){
        try{
            //Попытка считать конфигурацию из файла
            Scanner fileIn = new Scanner(new FileReader(ConfFile));
            IPSereverDB = fileIn.nextLine();
            PortServerDB = fileIn.nextInt();
            myPort = fileIn.nextInt();
            lenPath = fileIn.nextInt();
            fileIn.close();
        }
        catch(FileNotFoundException e){                
            //Файла не нашлось, пишем сами
            try{
                PrintWriter fileOut = new PrintWriter(new FileWriter(ConfFile));
                fileOut.println(IPSereverDB);            
                fileOut.println(PortServerDB);
                fileOut.println(myPort);
                fileOut.println(lenPath);
                fileOut.close();
            }catch(Exception ex){}
        }
        if(Started){
            //Ищем свободный порт и создаём сокет
            boolean socket_created = false;       
            while(!socket_created)
            {
                try{
                    clientSoket = new ServerSocket(myPort);                                
                    socket_created = true;
                }
                catch(Exception e){
                    myPort++;
                }
            }
        }                
        boolean isReady = false;
        //Получаем список узлов от сервера каталогов
        try{  
           Socket servDB = new Socket(IPSereverDB, PortServerDB);
           ObjectOutputStream out = new ObjectOutputStream(servDB.getOutputStream());                                 
           out.writeUTF("list");                      
           out.flush();
           ObjectInputStream in = new ObjectInputStream(servDB.getInputStream());
           
           //узнаёт количество узлов и считывает их описание
           int n = in.readInt();           
           list = new NodeAddress[n];
           for(int i = 0; i < n; i++){
               list[i] = (NodeAddress) in.readObject();                      
           }
           in.close();
           out.close();
           servDB.close();
           isReady = true;
        }
        catch(Exception e)
        {tbOutput.setText("init error: "+e);}
        
        if(isReady){
            lenPath = Math.min(lenPath, list.length); //Определение длины пути
            path = genPath(lenPath);              //Выбор пути
            sendOnion();                          //Построение пути при помощи луковицы
            lbPort.setText("Клиент запущен на порту "+ myPort);
            if(Started){
                ListenerThread lt = new ListenerThread();            
                lt.start();
            }
        }
        else{
            tbOutput.setText("Error: Can not connect to serverDB");
        }                       
    }
    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jScrollPane1 = new javax.swing.JScrollPane();
        tbOutput = new javax.swing.JTextArea();
        lbPort = new javax.swing.JLabel();
        tbIP = new javax.swing.JTextField();
        ibAddress = new javax.swing.JLabel();
        tbPort = new javax.swing.JTextField();
        jScrollPane2 = new javax.swing.JScrollPane();
        tbInput = new javax.swing.JTextArea();
        btnReconnect = new javax.swing.JButton();
        btnSend = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("xCLIENTx");

        tbOutput.setColumns(20);
        tbOutput.setEditable(false);
        tbOutput.setRows(5);
        jScrollPane1.setViewportView(tbOutput);
        tbOutput.getAccessibleContext().setAccessibleParent(this);

        lbPort.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N
        lbPort.setText("Устанавливается соединение...");

        tbIP.setHorizontalAlignment(javax.swing.JTextField.CENTER);
        tbIP.setText("127.0.0.1");

        ibAddress.setText("Адрес получателя(IP, порт):");

        tbPort.setHorizontalAlignment(javax.swing.JTextField.CENTER);
        tbPort.setText("7161");

        tbInput.setColumns(20);
        tbInput.setRows(5);
        tbInput.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
        jScrollPane2.setViewportView(tbInput);

        btnReconnect.setText("Обновить маршрут");
        btnReconnect.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnReconnectActionPerformed(evt);
            }
        });

        btnSend.setText("Отправить");
        btnSend.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnSendActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jScrollPane2)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addGap(0, 0, Short.MAX_VALUE)
                        .addComponent(btnSend))
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(ibAddress)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(tbIP, javax.swing.GroupLayout.PREFERRED_SIZE, 95, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(tbPort, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addGap(0, 0, Short.MAX_VALUE))
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(btnReconnect)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 12, Short.MAX_VALUE)
                        .addComponent(lbPort)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(5, 5, 5)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(btnReconnect)
                    .addComponent(lbPort))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 164, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(tbIP, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(ibAddress)
                    .addComponent(tbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(btnSend)
                .addGap(6, 6, 6))
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void btnReconnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnReconnectActionPerformed
        lbPort.setText("Устанавливается соединение...");        
        myInit();
    }//GEN-LAST:event_btnReconnectActionPerformed
//Отправка сообщения
    private void btnSendActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSendActionPerformed
       try{
            String lastIp = tbIP.getText();
            int lastPort = Integer.parseInt(tbPort.getText());  
            byte res[] = tbInput.getText().getBytes(OnionConst.CharSet);
            SecureRandom rnd = new SecureRandom();
            Cipher E = Cipher.getInstance(OnionConst.SymmeetricKeyEncrypt + 
                    OnionConst.SymmetricEncryptMode);
            for(int i = path.length-1; i>=0; i--){
                int cur = path[i];               
                byte[] layer = Arrays.copyOfRange(
                        Inet4Address.getByName(lastIp).getAddress(),0,6);                
                layer[4] = (byte)(lastPort & 0xff);
                layer[5] = (byte) ((lastPort>>8) & 0xff);                
                byte[] tmp = new byte[layer.length + res.length];
                for(int j = 0; j < tmp.length; j++){
                    if(j<layer.length){
                        tmp[j] = layer[j];
                    }
                    else{
                        tmp[j] = res[j-layer.length];
                    }
                }
                byte IV[] = new byte[OnionConst.SKE_block_size_byte];
                rnd.nextBytes(IV);
                E.init(Cipher.ENCRYPT_MODE, keys[i], new IvParameterSpec(IV));                
                byte[] tmpRes = E.doFinal(tmp);
                res = new byte[IV.length + tmpRes.length];
                for(int index = 0; index < res.length; index++){
                    if(index<IV.length){
                        res[index] = IV[index];                    
                    }else{
                        res[index] = tmpRes[index - IV.length];
                    }
                }
                lastIp = list[cur].IP;
                lastPort = list[cur].port;  
            }
            toNodeOut.write(res);
            toNodeOut.flush();            
        }
       catch(Exception e){
            tbOutput.setText("Error: "+e);
        }
    }//GEN-LAST:event_btnSendActionPerformed

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /*
         * Set the Nimbus look and feel
         */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /*
         * If Nimbus (introduced in Java SE 6) is not available, stay with the
         * default look and feel. For details see
         * http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(Client.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(Client.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(Client.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(Client.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /*
         * Create and display the form
         */
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                new Client().setVisible(true);               
            }
        });
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton btnReconnect;
    private javax.swing.JButton btnSend;
    private javax.swing.JLabel ibAddress;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JLabel lbPort;
    private javax.swing.JTextField tbIP;
    private javax.swing.JTextArea tbInput;
    private javax.swing.JTextArea tbOutput;
    private javax.swing.JTextField tbPort;
    // End of variables declaration//GEN-END:variables
}
